AWS CDKを使ってStep FunctionsでFargateなECSタスクを実行するステートマシンを作成。例外処理も。
こんにちは。AWS事業本部コンサルティング部に所属している今泉(@bun76235104)です。
私はこれまでIaC(Infrastructure as Code)ツールとしてTerraformを使うことが多かったのですが、今回CDKを利用する機会に恵まれました。
そこで、皆大好き AWS Step FunctionsをCDKで組んでみようということで、ECSのタスク定義を動かしてみました。
フローについては後述しますが、以下のようなフローのステートマシンを作成します。
なお、以降の検証は以下環境で実施していますのでご留意ください。
Key | Value |
---|---|
OS | macOS Monterey |
Node Version | 18.12.1 |
CDK Version | 2.37.1 |
言語 | TypeScript |
下準備 VPCなどのリソース作成
今回ECSのRun Taskを実行させるため、以下のような構成を作成しました。
StepFunctionsからECS on Fargateでタスクを実行する形です。
今回はStepFunctionsの作成がメインとなるためVPCなどのリソースのコードについては割愛しますが、コード全文はこちらのリポジトリで閲覧できます。
なお今回はプライベートサブネットからFargateでコンテナを起動できるように、以下VPCエンドポイントを追加しています。
Fargate利用時に必要なVPCエンドポイントについてはこちらのブログをご確認ください。
CDKでStep Functions を定義する
作るものの確認
StepFunctionsを作成するためのクラスを定義していきます。
再掲となりますが、今回作成するStep Functionsのステートマシンは以下のとおりです。
Step1 OKRun
では以下のような定義のDockerコンテナを動かします。
FROM alpine:latest ENTRYPOINT [ "ash", "-c", "echo shuld end with code0 && true"]
true
コマンドにより、終了コード0で正常にコンテナが終了するイメージです。
はたまた、Step2 NGRun
では以下のような定義のDockerコンテナを動かします。
FROM alpine:latest ENTRYPOINT [ "ash", "-c", "echo shuld end with code1 && false"]
こちらは対照的に false
コマンドにより終了コード1でコンテナが異常終了するイメージです。
Step1 Fail
と Step2 Fail
はそれぞれのステップで失敗した場合の例外処理となっています。
まとめると以下のようになります。
Step1 OKRun
ではコンテナ(Fargate Task)が正常終了するはず- 正常終了するので
Step1 Fail
には進まないでほしい
- 正常終了するので
Step2 NGRun
ではコンテナ(Fargate Task)が異常終了する(終了コード1のため)- 異常終了するので
Step2 Fail
に進んでほしい
- 異常終了するので
進行してほしいルートはこうです。
やりたいことを確認できたので、CDKのコードをみていきます。
FargateタスクをStepFunctionsで実行するためのコード
CDKから必要なコードを抜粋した部分は以下のとおりです。
まず、StepFunctionsでFargateのタスクを実行するにあたり、VPCなどの情報が必要であるため、必要なパラメータを以下interfaceにまとめています。
import { Construct } from 'constructs'; import { Cluster } from 'aws-cdk-lib/aws-ecs'; import { Vpc } from 'aws-cdk-lib/aws-ec2'; import { FargateTaskDefinition } from 'aws-cdk-lib/aws-ecs'; export interface StepFunctionsParam { scope: Construct; vpc: Vpc; okTaskDef: FargateTaskDefinition; ngTaskDef: FargateTaskDefinition; cluster: Cluster; }
okTaskDef
がStep1 OKRun
で実行させたいコンテナ実行情報を持つタスク定義ngTaskDef
がStep2 NGRun
で実行させたいコンテナ実行情報を持つタスク定義
となっています。
また、ECSクラスタの情報も cluster
パラメータで渡しています。
次にこれらパラメータを受け取って、StepFunctionsのステートマシンを作成するクラスがこちらです。
import { Construct } from 'constructs'; import { aws_stepfunctions_tasks } from 'aws-cdk-lib'; import { Errors, IntegrationPattern, Pass, TaskStateBase, Fail, StateMachine, } from 'aws-cdk-lib/aws-stepfunctions'; import { EcsFargateLaunchTarget } from 'aws-cdk-lib/aws-stepfunctions-tasks'; import { StepFunctionInvokeAction } from 'aws-cdk-lib/aws-codepipeline-actions'; import { SecurityGroup } from 'aws-cdk-lib/aws-ec2'; import { TaskDefinition } from 'aws-cdk-lib/aws-ecs'; import { StepFunctionsParam } from './interfaces/step_functions_param'; import { Resource } from './abstract/resource'; export class StepFunc extends Resource { readonly params: StepFunctionsParam; constructor(params: StepFunctionsParam) { super(); this.params = params; } public createResources() { const sg = this.createSG(); // Step1 const okTask = this.getRunTaskParam('Step1 OKRun', this.params.okTaskDef, [ sg, ]); const failStep1 = new Fail(this.params.scope, 'Step1Fail'); okTask.addCatch(failStep1); // Step2 const ngTask = this.getRunTaskParam('Step2 NGRun', this.params.ngTaskDef, [ sg, ]); const failStep2 = new Fail(this.params.scope, 'Step2Fail'); ngTask.addCatch(failStep2); const definition = okTask.next(ngTask); // createSteate new StateMachine(this.params.scope, 'ExampleStepStateMachine', { definition, }); } private createSG(): SecurityGroup { // RunTaskの際に利用するSG const sg = new SecurityGroup(this.params.scope, 'RunTaskSG', { vpc: this.params.vpc, securityGroupName: 'stepExampleRuntaskSG', allowAllOutbound: true, }); return sg; } private getRunTaskParam( id: string, taskdef: TaskDefinition, sg: [SecurityGroup] ) { const runTask = new aws_stepfunctions_tasks.EcsRunTask( this.params.scope, id, { integrationPattern: IntegrationPattern.RUN_JOB, cluster: this.params.cluster, taskDefinition: taskdef, assignPublicIp: false, launchTarget: new EcsFargateLaunchTarget(), subnets: { subnets: this.params.vpc.isolatedSubnets, }, securityGroups: sg, } ); return runTask; } }
createResources
の部分が主要ロジックです。
public createResources() { // ECS RunTaskで利用するセキュリティグループを作成 const sg = this.createSG(); // Step1でタスク定義から実行するためのジョブを追加 const okTask = this.getRunTaskParam('Step1 OKRun', this.params.okTaskDef, [ sg, ]); // Step1の例外処理を追加 const failStep1 = new Fail(this.params.scope, 'Step1Fail'); okTask.addCatch(failStep1); // Step2でタスク定義をもとに実行するためのジョブを追加 const ngTask = this.getRunTaskParam('Step2 NGRun', this.params.ngTaskDef, [ sg, ]); // Step2の例外処理を追加 const failStep2 = new Fail(this.params.scope, 'Step2Fail'); ngTask.addCatch(failStep2); const definition = okTask.next(ngTask); // createSteate new StateMachine(this.params.scope, 'ExampleStepStateMachine', { definition, }); }
呼び出す側のクラスを抜粋すると以下のようになっています。
import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; import { Ecr } from './resources/ecr'; import { Network } from './resources/network'; import { Ecs } from './resources/ecs'; import { StepFunc } from './resources/step_functions'; export class StepExampleStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // ↑ここより上でVPCなどのリソースを作成しているが省略 // StepFunc const sf = new StepFunc({ scope: this, okTaskDef: ecs.okTaskDef, ngTaskDef: ecs.ngTaskDef, cluster: ecs.cluster, vpc: network.vpc, }); sf.createResources(); } }
呼び出し部分を省略せずに記載すると以下のようになっています。
VPCやECRなどの必要なリソースを先に作成して、StepFunctionsのリソース作成に利用しています。
import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; import { Ecr } from './resources/ecr'; import { Network } from './resources/network'; import { Ecs } from './resources/ecs'; import { StepFunc } from './resources/step_functions'; export class StepExampleStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // ECR const ecr = new Ecr(this); ecr.createResources(); // Network const network = new Network(this); network.createResources(); // ECS taskdef const ecs = new Ecs({ scope: this, okImage: ecr.getOKImage(), ngImage: ecr.getNGImage(), vpc: network.vpc, }); ecs.createResources(); // StepFunc const sf = new StepFunc({ scope: this, okTaskDef: ecs.okTaskDef, ngTaskDef: ecs.ngTaskDef, cluster: ecs.cluster, vpc: network.vpc, }); sf.createResources(); } }
以上がStepFunctionsに関連する部分の抜粋となります。
繰り返しとなりますがコード全文を確認したい場合以下リポジトリをご確認ください。
CDKでリソース作成 & StepFunctionsのステートマシンを実行してみる
それではCDKを実行してみます。
cdk synth
これによりCloudFormationの定義を確認し、問題なさそうですので以下コマンドでデプロイします。
cdk deploy
無事リソースが作成されました。
さっそく実行してみます!
始まりました...
想定どおり、Step2 NGRun
でコンテナが異常終了したため、Step2 Fail
で例外をキャッチしています!
なんとか意図どおりの挙動となりました。
まとめ
- CDKでFargateのタスクを実行するStepFunctionsのステートマシンを作ってみた
TypeScript+CDK
をVSCodeで開発する体験が良すぎる- 型補完がやばい(語彙力)
今回サンプルのコードは今時点の私が書いたコードであるため、美しない点が多々あると思いますので、是非もっと良い構成を考えて教えてください!